You Don't Know JS上卷Part1 Chapter1.作用域是什么

第1章 作用域是什么

  • 问题1:变量储存在哪里?
  • 问题2:程序需要时如何找到它们?

1.1 编译原理

JavaScript语言是“动态”或“解释执行”语言,但事实上是一门编译语言。但它不是提前编译的,编译结果也不能在分布式系统中移植。

传统编译语言流程中,程序在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析(Tokenizing/Lexing)

    将由字符组成的字符串分解成(对编程语言来说)有意义的代码块。

    1
    var a = 2;

    上面这段程序会被分解成以下词法单元:var、a、=、2、;。

    空格是否会被当做词法单元,取决于空格在这门语言中是否有意义。

  • 解析/语法分析(Parsing)

    将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数。这个数被称作抽象语法树(Abstract Syntax Tree, AST)。

    1
    var a = 2;

    以上代码的抽象语法树如下所示:

    • VariableDeclaration 顶级节点
      • Identifier 子节点,值为a
      • AssignmentExpression 子节点
        • NumericLiteral 子节点,字为2
  • 代码生成

    AST转换成可执行代码的过程。过程与语言、目标平台等相关。

    简单来说就是可以通过某种方法将var a = 2;的AST转化为一组机器指令。用来创建一个叫做a的变量(包括分配内存等),并将一个值存储在a中。

1.2 理解作用域

1.2.1 演员表
  • 引擎:从头到尾负责整个JavaScript程序的编译和执行。
  • 编译器:负责语法分析和代码生成等
  • 作用域:负责收集并维护由所有声明的标识符(变量、函数)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
1.2.2 对话

var a = 2;存在2个不同的声明。

  • 1、编译器在编译时处理(var a):在当前作用域中声明一个变量(如果之前没有声明过)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    st=>start: Start
    e=>end: End
    op1=>operation: 分解成词法单元
    op2=>operation: 解析成树结构AST
    cond=>condition: 当前作用域存在变量a?
    op3=>operation: 忽略此声明,继续编译
    op4=>operation: 在当前作用域集合中声明新变量a
    op5=>operation: 生成代码
    st->op1->op2->cond
    cond(yes)->op3->op5->e
    cond(no)->op4->op5->e
  • 2、引擎在运行时处理(a = 2):在作用域中查找该变量,如果找到就对变量赋值。

1
2
3
4
5
6
7
8
9
10
11
12
st=>start: Start
e=>end: End
cond=>condition: 当前作用域存在变量a?
cond2=>condition: 全局作用域?
op1=>operation: 引擎使用这个变量a
op2=>operation: 引擎向上一级作用域查找变量a
op3=>operation: 引擎把2赋值给变量a
op4=>operation: 举手示意,抛出异常
st->cond
cond(yes)->op1->op3->e
cond(no)->cond2(no)->op2(right)->cond
cond2(yes)->op4->e
1.2.3 LHS和RHS查询

LR分别代表一个赋值操作的左侧和右侧,当变量出现在赋值操作的左侧时进行LHS查询,出现在赋值操作的非左侧时进行RHS查询。

  • LHS查询(左侧):找到变量的容器本身,然后对其赋值

  • RHS查询(非左侧):查找某个变量的值,可以理解为 retrieve his source value,即取到它的源值

1
2
3
4
5
function foo(a) {
console.log( a ); // 2
}

foo(2);

上述代码共有1处LHS查询,3处RHS查询。

  • LHS查询有:

    • 隐式的a = 2中,在2被当做参数传递给foo(…)函数时,需要对参数a进行LHS查询
  • RHS查询有:

    • 最后一行foo(...)函数的调用需要对foo进行RHS查询

    • console.log( a );中对a进行RHS查询

    • console.log(...)本身对console对象进行RHS查询

1.3 作用域嵌套

遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没有找到,查找过程都会停止。

1.4 异常

ReferenceError和作用域判别失败相关,TypeError表示作用域判别成功了,但是对结果的操作是非法或不合理的。

  • RHS查询在作用域链中搜索不到所需的变量,引擎会抛出ReferenceError异常。
  • 非严格模式下,LHS查询在作用域链中搜索不到所需的变量,全局作用域中会创建一个具有该名称的变量并返还给引擎。
  • 严格模式下(ES5开始,禁止自动或隐式地创建全局变量),LHS查询失败会抛出ReferenceError异常
  • 在RHS查询成功情况下,对变量进行不合理的操作,引擎会抛出TypeError异常。(比如对非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性)

1.5 小结

var a = 2被分解成2个独立的步骤。

  • 1、var a在其作用域中声明新变量
  • 2、a = 2会LHS查询a,然后对其进行赋值